iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0

Day 2:逼 AI 驗證,而不是接受它的記憶

昨天結尾我說,那個 = NULL 的真相是「逼 AI 把原始碼攤開來看」才得出的。今天把整個過程完整拆開——因為真正值得學的不是結論,而是「怎麼逼」的那套步驟。

而且我要先老實說一件昨天沒講的事:問 AI 之前,我其實已經知道答案了。

我不是在找答案,我是在測試它

情境是這樣:一個舊專案要從 Sequelize 4 升到 6,跨了兩個大版本。

(先給不熟的讀者一句話:Sequelize 是 Node.js 上最常用的 ORM 之一。ORM 的作用是讓你用寫物件的方式操作資料庫,它在背後自動幫你把這些操作翻譯成 SQL——方便,但也代表「你寫的」和「它實際送出的 SQL」之間隔了一層,這層翻譯正是今天故事的關鍵。)

升級這種舊專案,最怕的不是會報錯的地方,而是行為悄悄變了、但程式照跑不誤的地方。我盯上的疑點是:當查詢條件的值是 undefined 時,Sequelize 會怎麼處理?

這在舊程式碼裡是家常便飯:

const where = {
  status: req.query.status,   // 使用者沒帶這個參數時,就是 undefined
  type: req.query.type
};
const result = await Model.findAll({ where });

req.query.statusundefined,這個條件會變成什麼?

這裡要先講一個關鍵背景,否則你會覺得這段程式碼寫得很隨便:在 Sequelize 4.x 的時代,這樣寫是完全合理、甚至是社群主流的做法。 因為 4.x 遇到值為 undefined 的條件,行為就是「直接忽略這個條件」——它不會出現在最後的 SQL 裡。

這帶來一個很方便的寫法:你可以把一堆「使用者可能有帶、也可能沒帶」的查詢參數,全部一股腦丟進 where,沒帶到的自然被忽略,剩下有值的就自動組成查詢條件。不用寫一堆 if (status) where.status = status 的判斷,程式碼乾淨得很。所以在舊專案裡,這種寫法到處都是——它不是偷懶,是當年的正確答案。

問題就出在這裡:如果 4.x「忽略」的行為,到了 6.x 變了樣,那專案裡成千上百個依賴這個行為的動態查詢,全部都會默默出錯——而且不會報錯,因為語法一直都對。

所以我做的第一件事,不是問 AI,是自己寫了一支最小的測試腳本跑跑看。 Sequelize 可以把它實際送出的 SQL 印出來,與其猜,不如直接看:

const { Sequelize, DataTypes } = require('sequelize');

const sequelize = new Sequelize('sqlite::memory:', {
  logging: console.log   // 把實際送出的 SQL 印出來
});

const T = sequelize.define('T', { status: DataTypes.STRING });

(async () => {
  await sequelize.sync();
  await T.findAll({ where: { status: undefined } });  // 關鍵測試
})();

在實際要升級的那個版本上跑,它印出來的 SQL 是:

WHERE `T`.`status` = NULL

我當下就知道這是個大坑(原因等一下說)。手上有了這個客觀事實,我才去問 AI——不是為了找答案,是想看看:如果一個工程師遇到這個問題、又剛好沒像我一樣先測,AI 會把他帶到哪裡去。

AI 給的兩個答案,都會害到那個人

我問 AI:Sequelize 6 遇到 undefined 的查詢條件會怎麼處理?

第一個答案:「新版會忽略這個條件。」

聽起來很合理。但我知道它錯了——因為我的腳本明明印出了 = NULL,條件沒有被忽略。

我沒有直接戳破,而是換個問法多問一句:「是『忽略』,還是『視為 NULL 來查詢』?」——想看它會不會堅持。結果它改口了,說是後者,會被當成 IS NULL 處理。

這一改口很關鍵。我沒有給它任何新資訊,只是把選項攤開,它就從 A 跳到 B。這證實了我的猜測:它的答案不是查證來的,是順著對話生成的。而且更糟——它第二個答案 IS NULL 聽起來比第一個更專業、更具體,一個沒有實測資料的工程師,很可能就在這裡被說服了。

把三種說法並排,你會看到這個差異有多致命:

說法 實際 SQL 查詢結果
AI 第一次「忽略」 條件不出現 正常撈出符合其他條件的資料
AI 第二次「IS NULL」 WHERE status IS NULL 撈出 status 為空的資料
實測真相「= NULL」 WHERE status = NULL 永遠零筆

為什麼 = NULL 是最壞的結果

= NULL 在 SQL 標準裡是一個永遠不成立的條件。

原因是 SQL 的三值邏輯:NULL 代表「未知」,任何值跟「未知」做等號比較,結果都不是 truefalse。判斷欄位是否為空,正確寫法是 IS NULL,不是 = NULL

所以 WHERE status = NULL 的實際效果是:這個查詢永遠回傳零筆。 不報錯、不拋例外,只是讓某些查詢莫名其妙查不到資料。使用者說「明明有資料卻查不到」,你在程式碼裡怎麼看都看不出問題,因為語法完全正確。這種 bug 可以吃掉你一整天。

三種結果裡,「忽略」還能運作、「IS NULL」至少行為明確,唯獨實測的 = NULL 是那個會靜默出錯、最難抓的。而 AI 兩次都沒指向它。

(補一句:不同版本、不同資料庫方言的處理不完全一樣,有些情況會幫你轉成 IS NULL。這正是重點——行為依賴版本和方言,所以不能靠記憶,只能在你要升級的那個版本上實測。)

最後,我要它去看原始碼

到這裡,事實已經很清楚了——我有腳本、有 SQL、有結論。

看到 = NULL 的那一刻,我心裡已經有底了。所以我對它說了那句話:「不要順著我的話說,去看原始碼。」

它這才把對應版本處理 where 條件的原始碼攤開,開始真正解釋這個行為的成因——哪個版本、哪段邏輯、為什麼組出來的是 = NULL。到這一步,它才從「順著我生成答案」切換成「根據事實解釋」,昨天 Day 1 收尾的那個畫面,就是這裡。

這段其實已經是結果、不是今天的重點——真正的重點是它前面那整套:先自己實測拿到事實,再拿事實去測 AI。 讀原始碼只是收尾的確認,讓它在正確的事實上把成因補完整而已。

這一天真正想講的方法

回頭看,整件事有一個清楚的先後:

  1. 先取得客觀事實(自己跑腳本,不依賴任何人的說法)
  2. 拿事實去測 AI,觀察它是查證還是揣測(會因問法改答案的,就是揣測)
  3. 用事實逼它對齊(把實測結果和原始碼攤給它看,拉它回到推理模式)

這裡面藏著一個我後來一直用的準則:AI 該是被事實驗證的一方,不是提供事實的一方。

它很擅長幫你寫那支驗證腳本、很擅長在你給了正確事實後把成因解釋得清清楚楚——這些都是它的強項,是它的「1」。但「事實到底是什麼」,這件事不能外包給它。你得自己去拿,再回頭要求它在這個事實上工作——這是人補上的那一份,也是讓協作產生加成的地方。

讓它當你的手,不要讓它當你的眼睛。這是這個系列會反覆出現的第一條紀律。

明天換一個角度:當 AI 給出一個「聽起來完全正確的優化建議」時,為什麼規模是你該追問的第一件事——一個關於 Promise.all 的故事,一個正確的建議如何在真實資料量下變成壓垮資料庫的一擊。


上一篇
Day 1:AI 負責答,我負責讓答案可信——一個公部門工程師的協作紀律
下一篇
Day 03:一個正確的建議,如何變成壓垮資料庫的一擊
系列文
AI 負責答,我負責讓答案可信——公部門工程師的 30 天 Claude 協作紀律6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言